package com.gitee.wsl.net.server

import com.gitee.wsl.Lib
import com.gitee.wsl.ext.json.defaultJson
import com.gitee.wsl.ext.string.encryptMD5
import com.gitee.wsl.net.base.CommonJsonResult
import com.gitee.wsl.net.base.ErrorResult
import com.gitee.wsl.net.exceptions.Exceptions
import com.gitee.wsl.resource.local.URI
import com.gitee.wsl.transform.validator.Validator
import io.ktor.http.ContentType
import io.ktor.http.ContentType.Application.ProtoBuf
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.contentType
import io.ktor.server.request.header
import io.ktor.server.request.host
import io.ktor.server.request.receive
import io.ktor.server.request.receiveText
import io.ktor.server.request.userAgent
import io.ktor.server.response.respond
import io.ktor.server.response.respondRedirect
import io.ktor.server.response.respondText
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.protobuf.ProtoBuf


/**
 * Get request with validate
 */
suspend inline fun <reified T : Any,V:Validator<T,V>> ApplicationCall.receiveValidate(validator: Validator<T,V>): T {
    val request = try {
        receive<T>()
    } catch (ex: Exception) {
        throw Exceptions.BadRequest()
    }

    if (validator.validate(request)) {
        return request
    } else {
        throw RuntimeException(validator.message(request).value)
    }

    /*val validate = Validation.buildDefaultValidatorFactory().validator.validate(request)

    if (validate.isEmpty()) {
        return request
    } else {
        throw Exceptions.UnprocessableEntity(validate)
    }*/

}

/**
 * Helper to redirect to the referer
 */
suspend fun ApplicationCall.redirectToReferrer() {
    return respondRedirect(this.request.headers["Referer"] ?: "", false)
}



/**
 * Get value from params with validate
 */
fun ApplicationCall.getNumberParam(key: String = "id"): Int = parameters[key]
    ?.toIntOrNull() ?: throw Exceptions.NotFound()

/**
 * Get value from params with validate
 */
fun ApplicationCall.getStringParam(key: String = "name"): String = parameters[key]
    ?: throw  Exceptions.NotFound()

/**
 * Get value from params with validate
 */
infix fun ApplicationCall.getQueryParam(key: String) =
    request.queryParameters[key] ?: throw  Exceptions.NotFound()

/**
 * Get value from params with validate
 */
fun ApplicationCall.getNumbersQueryParam(key: String = "ids") =
    request.queryParameters.entries().find { it.key == key }?.value?.mapNotNull { it.toIntOrNull() }
        ?: throw  Exceptions.NotFound()

/**
 * Get value from params with validate
 */
fun ApplicationCall.getDoublesQueryParam(key: String = "ids") =
    request.queryParameters.entries().find { it.key == key }?.value?.mapNotNull { it.toDoubleOrNull() }
        ?: throw  Exceptions.NotFound()

/**
 * Get unique connect
 */
fun ApplicationCall.connectKey() = request.host().encryptMD5()


/*
suspend inline fun ApplicationCall.respondCss(builder: CssBuilder.() -> Unit) {
    this.respondText(CssBuilder().apply(builder).toString(), ContentType.Text.CSS)
}*/

@OptIn(ExperimentalSerializationApi::class)
val PROTO = ProtoBuf {
    serializersModule = EmptySerializersModule()
}


val defaultRpcProtocol = "json"

suspend inline fun <reified T> ApplicationCall.respondJson(
    data: T? = null, info: String? = null,
    code: Int = 0, status: HttpStatusCode = HttpStatusCode.OK
) {
    this.respond(
        Lib.defaultJson.encodeToString(
            CommonJsonResult(
                status.value,
                info = info ?: status.description,
                code = code,
                data = data
            )
        )
    )
}

@OptIn(ExperimentalSerializationApi::class)
suspend inline fun <reified T> ApplicationCall.respondProtobuf(
    data: T? = null, info: String? = null,
    code: Int = 0, status: HttpStatusCode = HttpStatusCode.OK
) {
    this.respond(
        PROTO.encodeToByteArray(
            CommonJsonResult(
                status.value,
                info = info ?: status.description,
                code = code,
                data = data
            )
        )
    )
}

suspend inline fun <reified T> ApplicationCall.respondData(
    data: T? = null, info: String? = null,
    code: Int = 0, status: HttpStatusCode = HttpStatusCode.OK
) {
    val type = this.request.contentType().contentSubtype
    if (type.contains("json")) {
        respondJson(data, info, code, status)
    } else if (type.contains("protobuf")) {
        respondProtobuf(data, info, code, status)
    } else {
        if (defaultRpcProtocol == "json")
            respondJson(data, info, code, status)
        else
            respondProtobuf(data, info, code, status)
    }
}

suspend fun ApplicationCall.respondInfo(
    info: String? = null,
    code: Int = 0, status: HttpStatusCode = HttpStatusCode.OK
) {
    respondData<Unit>(null, info, code, status)
}

suspend fun ApplicationCall.respondJsonText(
    jsonText: String,
    status: HttpStatusCode = HttpStatusCode.OK
) {
    this.respondText(jsonText, ContentType.Application.Json, status)
}

/**
 * Respond a success message to user
 * @param msg message
 * @param data return data. If data is [URI] it will be equals to redirectUrl
 * @param redirectURI Redirect user to this url, will be attached to Header "X-Redirect-Location" Redirection is only available if useragent is a valid user browser.
 */
suspend inline fun <reified T> ApplicationCall.respondSuccess(
    msg: String? = null,
    data: T? = null,
    redirectURI: URI? = null,
    statusCode: HttpStatusCode = HttpStatusCode.TemporaryRedirect
) {
    respondData(data, msg)
}

suspend inline fun ApplicationCall.respondSuccess(
    msg: String? = null,
    redirectURI: URI? = null,
    statusCode: HttpStatusCode = HttpStatusCode.TemporaryRedirect
) {
    respondData<Unit>(null, msg)
}


fun ApplicationCall.isUserBrowserRequest(): Boolean {
    val userAgent = this.request.userAgent()
    return userAgent != null && userAgent.contains("Mozilla", true) &&
            this.request.header("X-Requested-With") == null
}



suspend fun ApplicationCall.respondError(
    status: HttpStatusCode,
    exception: Throwable? = null,
    isDebug: Boolean = false
) {
    var info = ""
    var trace = ""

    if (exception != null) {
        if (isDebug) {
            info = "${exception.message}"
            trace = exception.stackTraceToString()
        } else {
            if (status.value in 400..499 || status.value != 501)
                info = exception.message ?: exception.hashCode().toString()
            else
                info = exception.hashCode().toString()
        }
    }

//    if (isUserBrowserRequest()) {
//        this.respond(code, FreeMarkerContent("error.ftl", mapOf(
//                "code" to code.value,
//                "description" to code.description,
//                "info" to StringEscapeUtils.escapeHtml4(info),
//                "trace" to trace,
//                "isDebugMode" to ServerEnv.DebugMode,
//                "redirectUrl" to redirectURI?.appendQuery("msg=$info&code=${code.value}&description=${code.description}")?.toString()
//        )))
//    } else {
    respondData(
        info = info,
        status = status,
        code = if (exception is Exceptions) exception.status.value else if (status.value in 200..299) 0 else status.value,
        data = ErrorResult(
            exception = exception!!::class.simpleName ?: "",
            exceptionFullName = exception::class.qualifiedName ?: "",
            trace = trace
        )
    )
//    }
}


suspend fun ApplicationCall.receiveBytes(): ByteArray {
    return this.receive<ByteArray>()
}

@OptIn(ExperimentalSerializationApi::class)
suspend inline fun <reified T> ApplicationCall.receiveProtobuf(): T {
    return PROTO.decodeFromByteArray(receiveBytes())
}

suspend inline fun <reified T> ApplicationCall.receiveJson(): T {
    return Lib.defaultJson.decodeFromString(receiveText())
}

@OptIn(ExperimentalSerializationApi::class)
suspend inline fun <reified T> ApplicationCall.receiveData(): T {
    val type = request.contentType().contentSubtype
    return if (type.contains("json")) {
        Lib.defaultJson.decodeFromString(receiveText())
    } else if (type.contains("protobuf")) {
        PROTO.decodeFromByteArray(receiveBytes())
    } else {
        if (defaultRpcProtocol == "json")
            Lib.defaultJson.decodeFromString(this.receiveText())
        else
            PROTO.decodeFromByteArray(receiveBytes())
    }
}